﻿//The MIT License(MIT)

//Copyright(c) 2015 Martin Chrzan

//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace XmlAutoComplete
{
    /// <summary>
    /// Interaction logic for SuggestionTextBox.xaml
    /// </summary>
    public partial class SuggestionTextBox : UserControl
    {
        #region public properties
        /// <summary>
        /// auto close elements on typing ">" ?
        /// </summary>
        /// <returns></returns>
        public bool ElementAutoComplete { get; set; }

        /// <summary>
        /// Should be added namespace into elements automatically? 
        /// </summary>
        /// <returns></returns>
        public bool AppendNamespace { get; set; }

        private IParser parser;
        /// <summary>
        /// Parser to be used for a providing autocomplete functionality
        /// </summary>
        /// <returns></returns>
        public IParser Parser
        {
            get { return parser; }
            set
            {
                parser = value;
                Tree = value.GetTree();
                //setup suggestion engine
                SuggestionEngine = new XmlSuggestionEngine(Tree);
            }
        }

        #endregion

        #region private properties
        
        /// <summary>
        /// List of items that will be displayed as suggestion
        /// </summary>
        /// <returns></returns>
        private List<AutocompleteItem> SuggestionItems { get; set; }

        ISuggestionEngine SuggestionEngine { get;set; }

        private DecisionTree Tree { get; set; }

        private DecisionTree CurrentElement { get; set; }

        private char CurrentOpeningCharacter { get;set; }

        #endregion

        #region const strings

        const string START_ELEMENT = "<";
        const string END_ELEMENT = ">";
        const string SIMPLE_END_ELEMENT = "/>";
        const string COMPLEX_END_ELEMENT = "</";

        #endregion


        public string Text
        {
            get { return Textbox.Text; }
            set { Textbox.Text = value; }
        }

        /// <summary>
        /// clear textbox
        /// </summary>
        public void Clear()
        {
            Textbox.Clear();
            openedElementPosition = -1;
        }


        //keep position of last start element
        int openedElementPosition =-1;

        public SuggestionTextBox()
        {
            InitializeComponent();

            this.Loaded += FrameworkElement_Loaded;
        }

        bool suppressTextChanges;
        private void Textbox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (SuggestionEngine == null)
            {
                base.OnPreviewKeyDown(e);
                return;
            }

            if (e.Key == Key.Enter)
            {
                if (popup.IsOpen && popupList.SelectedIndex >= 0)
                {
                    e.Handled = true;
                }
                SubmitItem();
            }
            else if (e.Key == Key.Down)
            {
                if (popup.IsOpen)
                {
                    if (popupList.SelectedIndex < popupList.Items.Count - 1)
                        popupList.SelectedIndex++;
                    e.Handled = true;

                    popupList.ScrollIntoView(popupList.Items.GetItemAt(popupList.SelectedIndex));
                }
            }
            else if (e.Key == Key.Up)
            {
                if (popup.IsOpen)
                {
                    if (popupList.SelectedIndex > 0)
                        popupList.SelectedIndex--;
                    e.Handled = true;

                    if (popupList.SelectedIndex >= 0)
                        popupList.ScrollIntoView(popupList.Items.GetItemAt(popupList.SelectedIndex));
                }

            }

            else if (((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.Space))
            {
                OpenSuggestionPopup(Textbox.Text);
                e.Handled = true;
            }

            else if (e.Key == Key.Space)
            {
                CurrentOpeningCharacter = ' ';
                //get how will text look like after space is processed - now it is not entered, only after this event
                var text = Textbox.Text;
                text = text.Insert(Textbox.CaretIndex, " ");

                OpenSuggestionPopup(text);
            }

            else if (e.Key == Key.Escape)
            {
                if (popup.IsOpen)
                    popup.IsOpen = false;
                e.Handled = true;
            }
        }

        private void OpenSuggestionPopup(string text)
        {
            var items = SuggestionEngine.GetAttributes(text, Textbox.CaretIndex + 1);
            if (items != null && items.Count > 0)
            {
                SuggestionItems = items.Select(it => new AutocompleteItem(it)).ToList();
                if (SuggestionItems != null)
                    OpenSuggestions(SuggestionItems);
            }
        }


        /// <summary>
        /// Submit selected item in suggestion list after pressing enter or double click
        /// </summary>
        private void SubmitItem()
        {
            if (popup.IsOpen && popupList.SelectedIndex >= 0)
            {
                var offset = Textbox.VerticalOffset;
                var index = getLastCharacter(CurrentOpeningCharacter);

                //if we are inside attribute value, search for the next " otherwise search for next space
                var searchFor = ' ';
                if (CurrentOpeningCharacter == '\"')
                    searchFor = CurrentOpeningCharacter;

                var ending = Textbox.CaretIndex;

                var item = (popupList.SelectedValue as AutocompleteItem).RealName;

                //to prevent firing on text changed
                suppressTextChanges = true;
                //remove already writen text
                Textbox.Text = Textbox.Text.Remove(index, ending - index);
                //insert submited item from autocomplete list
                Textbox.Text = Textbox.Text.Insert(index, item);

                suppressTextChanges = false;
                //Textbox.Text += item.Substring(length);

                var openPopup = false;
                //only on adding attribute
                if (CurrentOpeningCharacter == ' ')
                {
                    Textbox.Text = Textbox.Text.Insert(index + item.Length, "=\"\"");
                    Textbox.CaretIndex = index + item.Length + 2;

                    SuggestionItems = SuggestionEngine.GetAttributeValues(Textbox.Text, Textbox.CaretIndex, item).Select(it => new AutocompleteItem(it)).ToList();
                    if (SuggestionItems.Count > 0)
                    {
                        OpenSuggestions(SuggestionItems);
                        openPopup = true;
                        CurrentOpeningCharacter = '\"';
                    }
                }
                else
                {
                    Textbox.CaretIndex = index + item.Length;
                    var oldCaretIndex = Textbox.CaretIndex;

                    if (AppendNamespace && !string.IsNullOrEmpty((popupList.SelectedValue as AutocompleteItem).Namespace))
                    {
                        var namespaceValue = " xmlns=\"" + (popupList.SelectedValue as AutocompleteItem).Namespace + "\"";

                        Textbox.Text = Textbox.Text.Insert(Textbox.CaretIndex, namespaceValue);

                        Textbox.CaretIndex = oldCaretIndex + namespaceValue.Length;
                    }
                }

                popup.IsOpen = openPopup;

                Textbox.ScrollToVerticalOffset(offset);
            }
        }

        /// <summary>
        /// catch actualy writen characters
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Textbox_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            if (SuggestionEngine == null)
                return;

            if (e.Text == START_ELEMENT)
            {
                openedElementPosition = Textbox.CaretIndex;

                //opening character is now < 
                CurrentOpeningCharacter = START_ELEMENT[0];

                SuggestionItems = SuggestionEngine.GetElements(Textbox.Text, Textbox.CaretIndex);
                if (SuggestionItems.Count>0)
                    OpenSuggestions(SuggestionItems);

              //  e.Handled = false;
            }

            if (e.Text == END_ELEMENT)
            {
                var oldCaretIndex = Textbox.CaretIndex;
                var offset = Textbox.VerticalOffset;
                //remove any selected text - we need to do that because we are handling all events in this case!
                if (Textbox.SelectionLength > 0)
                {
                    Textbox.Text = Textbox.Text.Remove(Textbox.SelectionStart, Textbox.SelectionLength);
                }

                //append end element
                Textbox.Text = Textbox.Text.Insert(oldCaretIndex, END_ELEMENT);

                //caret index is updated after this event
                var actualCaretIndex = oldCaretIndex + 1;

                Textbox.CaretIndex = actualCaretIndex;

                openedElementPosition = getLastCharacter(START_ELEMENT[0])-1;

                var elementNameEnd = getFirstCharacter(' ', openedElementPosition);
                if (elementNameEnd < openedElementPosition + 1 || elementNameEnd > oldCaretIndex)
                    elementNameEnd = oldCaretIndex;

                var elementName = Textbox.Text.Substring(openedElementPosition + 1, elementNameEnd - (openedElementPosition + 1));
                var closingElement = COMPLEX_END_ELEMENT + elementName + END_ELEMENT;
                //check if element is not simple one - closed 
                if (ElementAutoComplete && Textbox.Text[actualCaretIndex - 2] != '/' && Textbox.Text[actualCaretIndex - 2] != '?' && openedElementPosition > -1 && Textbox.Text[openedElementPosition] != '/' &&
                    elementName.Trim().Length>0 && elementName.Trim().Last() != END_ELEMENT[0]
                    && getFirstWordFromPosition(actualCaretIndex, END_ELEMENT[0]) != closingElement)
                    Textbox.Text = Textbox.Text.Insert(actualCaretIndex, Environment.NewLine + closingElement);

                //no opened element
                CurrentElement = null;
                
                //set caret in the middle of two elements right after closing first one
                Textbox.CaretIndex = actualCaretIndex;
                //we dont want to call other events 
                e.Handled = true;
                //close suggestion popup
                popup.IsOpen = false;

                Textbox.ScrollToVerticalOffset(offset);
            }
        }

        /// <summary>
        /// Method to get first word from given position
        /// </summary>
        /// <param name="position">position to start search from</param>
        /// <param name="barrierChar">if we want to stop when we reach given char - set to null if you want to search till the end of text</param>
        /// <returns></returns>
        private string getFirstWordFromPosition(int position, char? barrierChar=null)
        {
            StringBuilder word = new StringBuilder();

            for (int i = position; i < Textbox.Text.Length; i++)
            {
                var a = Textbox.Text[i];
                if (!char.IsSeparator(a) && !char.IsWhiteSpace(a))
                    word.Append(a);
                if (barrierChar != null && a == barrierChar)
                    break;
            }

            return word.ToString();
        }


        /// <summary>
        /// get position after first index of a given character that is on the left side from the current caret position
        /// </summary>
        /// <returns></returns>
        private int getLastCharacter(char characterToFind)
        {
            for (int i = Textbox.CaretIndex - 1; i >= 0; i--)
            {
                if (Textbox.Text[i] == characterToFind)
                {
                    return i + 1;
                }
            }
            return 0;
        }


        /// <summary>
        /// get position after first index of a given character that is on the right side from the current caret position
        /// </summary>
        /// <param name="characterToFind"></param>
        /// <param name="position">from where we should search for a given character, if null caretindex will be used</param>
        /// <returns></returns>
        private int getFirstCharacter(char characterToFind, int? position=null)
        {
            var start = Textbox.CaretIndex;
            if (position != null)
                start = (int)position;

            for (int i = start; i < Textbox.Text.Length; i++)
            {
                if (Textbox.Text[i] == characterToFind)
                {
                    return i;
                }
            }
            return 0;
        }


        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (suppressTextChanges) return;

            var starting = getLastCharacter(CurrentOpeningCharacter);
            var end = Textbox.CaretIndex; //getFirstCharacter(CurrentOpeningCharacter);

            string currentText = "";
            if (end >= starting)
                currentText = Textbox.Text.Substring(starting, end - starting).Trim();
            else
                currentText = Textbox.Text.Substring(starting);

            if (popup.IsOpen)
            {
                var i = 0;
                bool match = false;
                //scroll to the begging if there is nothing written
                if (currentText.Length == 0)
                {
                    if (popupList.Items.Count > 0)
                    {
                        popupList.ScrollIntoView(popupList.Items[0]);
                        popupList.SelectedIndex = i;
                        match = true;
                    }
                }
                else
                {
                    bool firstMatch = true;
                    int numberOfHidden = 0;
                    foreach (AutocompleteItem item in popupList.Items)
                    {
                        item.ItemVisibility = Visibility.Visible;
                        if (item.Item.ToLower().StartsWith(currentText.ToLower()) || item.Item.ToLower().Contains(currentText.ToLower()))
                        {
                            //select first match only
                            if (firstMatch)
                            {
                                popupList.SelectedIndex = i;
                                popupList.ScrollIntoView(popupList.Items.GetItemAt(popupList.SelectedIndex));
                            }
                            match = true;
                            firstMatch = false;

                        }
                        else
                        {
                            if (currentText.Length > 1)
                            {
                                item.ItemVisibility = Visibility.Collapsed;
                                numberOfHidden++;
                            }
                        }

                        i++;
                    }

                    if (numberOfHidden == popupList.Items.Count)
                        popup.IsOpen = false;
                }

                if (!match)
                    popupList.SelectedIndex = -1;
            }
        }

        private void OpenSuggestions(List<AutocompleteItem> options)
        {
            Point relativePoint = Textbox.TransformToAncestor(Application.Current.MainWindow).Transform(new Point(0, 0));

            var rect = Textbox.GetRectFromCharacterIndex(Textbox.CaretIndex, true);
      
            Rect rectan = new Rect(rect.Left , rect.Top , rect.Width, rect.Height);
            popup.PlacementRectangle = rectan;

            popup.IsOpen = true;

            popupList.Items.Clear();

            options.Sort();
            options.ForEach(it => popupList.Items.Add(it));

            //popup.Focus();
            popupList.IsTextSearchCaseSensitive = false;
            //popupList.Focus();
            popupList.SelectedIndex = 0;
        }

        #region handling popup movement on window location changed + size changed

        private void FrameworkElement_Loaded(object sender, RoutedEventArgs e)
        {
            var window = FindParent<Window>(this);

            if (null != window)
            {
                window.LocationChanged += delegate (object sender2, EventArgs args)
                {
                    var offset = popup.HorizontalOffset;
                    // "bump" the offset to cause the popup to reposition itself
                    //   on its own
                    popup.HorizontalOffset = offset + 1;
                    popup.HorizontalOffset = offset;
                };
                // Also handle the window being resized (so the popup's position stays
                //  relative to its target element if the target element moves upon 
                //  window resize)
                window.SizeChanged += delegate (object sender3, SizeChangedEventArgs e2)
                {
                    var offset = popup.HorizontalOffset;
                    popup.HorizontalOffset = offset + 1;
                    popup.HorizontalOffset = offset;
                };
            }
        }

        public static T FindParent<T>(DependencyObject child) where T : DependencyObject
        {
            //get parent item
            DependencyObject parentObject = VisualTreeHelper.GetParent(child);

            //we've reached the end of the tree
            if (parentObject == null) return null;

            //check if the parent matches the type we're looking for
            T parent = parentObject as T;
            if (parent != null)
                return parent;
            else
                return FindParent<T>(parentObject);
        }

        #endregion

        private void label_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            SubmitItem();
        }
    }

}
